クラスメソッドメンバーズポータルのバックエンドAPIをリプレイスしました

クラスメソッドメンバーズポータルのバックエンドAPIをリプレイスしました

Clock Icon2024.06.18

AWS事業本部サービス開発室の佐藤です。クラスメソッドメンバーズポータルのバックエンドAPIの開発を担当しています。

はじめに

弊社ではクラスメソッドメンバーズ(以下メンバーズ)と呼ばれるAWSの利用費割引や請求代行、コンサルティング、セキュリティ、24/365サポートなどAWSに関することをおまかせしていただけるサービスを提供しています。その中でメンバーズに契約いただいたお客様に対して、クラスメソッドメンバーズポータル(以下、CMP)というAWSアカウントリソースの利用状況を可視化できるWebサービスを提供しています。今回、CMPで利用しているバックエンドAPIをリプレイスしたのでその背景とリプレイス方法について簡単に紹介したいと思います。

リプレイスの背景

CMPは契約しているお客様向けに2013年頃から提供しているWebサービスで、バックエンドはSpring Boot, Javaで実装されています。歴史の長いサービスのためSpring Bootのフレームワークのバージョンや各種利用しているライブラリが古く継続的なアップデートができない状態になっていました。他にもCMPのフロントエンドのリプレイス認証基盤の刷新も同時並行で進んでいて、バックエンドにも大幅なコード変更が必要ということも背景にありました。

いくつか技術選定の候補はありましたが、既存のSpring Boot, Javaの資産をなるべく流用したかったため、リプレイス先はサーバーサイドKotlin(Spring Boot)に決まりました。

フロントエンドのリプレイスについても記事がありますので、よろしければこちらもご参照ください。

https://dev.classmethod.jp/articles/replace-members-frontend-2024/

https://dev.classmethod.jp/articles/replace-classmethod-members-internal-frontend-2024/

環境

旧API

ライブラリ バージョン
Java 11
Spring Boot 2.1.9
Gradle 6.9.2

新API

ライブラリ バージョン
Kotlin 1.9.23
Spring Boot 3.1.11
Gradle 7.6

リプレイスの方針

KotlinはJavaのコードを混在させることができるため、JavaからKotlinへのリプレイスには以下のような方針があると思います。

  • 既存のJavaコードを徐々にKotlinに移行しつつライブラリやフレームワークのバージョンを上げていく
  • 新規でKotlinのプロジェクトを作成し、既存のJavaコードを移行していく

今回のリプレイスでは後者を選択しました。

ちょうどSpring Boot 3.0のGAがアナウンスされていてSpring Boot 2.xからの移行が大変そうなこと、新旧APIで並行稼働しつつ段階的に移行していく必要があるなど、既存のコードベースを修正するのではなく、新規でリポジトリを作成しライブラリやフレームワークも最新にしつつ実装していくほうが早そうという結論になりこの方針にしました。

リプレイスの方法

新規Kotlinプロジェクトの作成

リプレイスするにあたって、まずは Spring Initializrを利用しSpring Boot 3.x + Kotlinの雛形を作成しました。必要なライブラリを画面ポチポチで作成できるので便利です。Projectは Gradle - Kotlin、Languageは Kotlin、Spring Bootは当時最新だった 3.1.2 を選択して作成しました。zipファイルがダウンロードできるのでローカルで展開してプロジェクトの準備は完了です。

既存のJavaプロジェクトから必要なファイルをコピー

新規でプロジェクトを作成したら、既存のコードから必要なJavaファイルを順次コピーしていきました。基本的にはController, Request, Response, Service, DTO, Entity, Repository, ConfigなどのSpringの主要なコンポーネントをコピーしました。さらにこの段階でJavaのファイルなどを棚卸し、不要なコードやファイルを削除していきました。

JavaからKotlinへの変換

Javaファイルをコピーしたら、随時Kotlinへの変換を行いました。変換についてはInteliJ IDEAにJavaからKotlinへの変換機能というのがあり、基本的にはこちらを利用して変換作業を行いました。Javaのファイルを右クリックして Convert Java File to Kotlin File を選択するかJavaのコードをコピーアンドペーストするだけで自動的に変換してくれます。

例えば、以下のようなLombokを利用したJavaのEntityクラスの場合

@ToString  
@NoArgsConstructor  
@AllArgsConstructor
@EqualsAndHashCode  
@Entity
@Table(name = "hoge")
public class Hoge implements Serializable {  
    @Id  
    @Column(name = "id")  
    @Getter  
    @Setter
    private var id: Long;

    @Getter  
    @Setter    
    private String name;  
}

Kotlinのデータ変換機能を利用すると以下のようなコードに変換してくれます。

@ToString  
@NoArgsConstructor  
@AllArgsConstructor  
@EqualsAndHashCode
@Entity
@Table(name = "hoge")
class Hoge : Serializable {  
    @Id  
    @Column(name = "id")  
    @Getter  
    @Setter
    private var id: Long? = null;

    @Column(name = "name")  
    @Getter  
    @Setter    
    private val name: String? = null  
}

ただ、このままだとLombokのアノテーションが付与されているので冗長です。また、すべてNullableなプロパティに変換されてしまうという問題もあります。Kotlinの場合はdata classがあるので、それを利用する形に修正します。すると以下のようになります。

@Entity
@Table(name = "hoge")
data class Hoge(
    @Id  
    @Column(name = "id")  
    val id: Long,

    @Column(name = "name")
    val name: String,
)

JavaだとアノテーションまみれだったのがKotlinだとdata classを利用することでスッキリ書くことができました。

他にも、Kotlinは基本的にJavaの機能がそのままつかえるので、JavaのStream APIやgetterなどはそのまま変換されます。

変換前のJavaコード:

@GetMapping("/{id}")
public Todo getTodo(@PathVariable Long id) {
    return todoList.stream()
        .filter(todo -> todo.getId().equals(id))
        .findFirst()
        .orElse(null);
}

変換後のKotlinコード:

@GetMapping("/{id}")
fun getTodo(@PathVariable id: Long?): Todo? {
    return todoList.stream()
        .filter { todo: Todo? -> todo.getId().equals(id) }
        .findFirst()
        .orElse(null)
}

このままでも問題なく動作するんですが、せっかくKotlinを利用しているのでKotlinの書き方にしたいですよね。Kotlinだと以下のように書くことができ、もともとのJavaコードよりかなりスッキリ書くことができました。

@GetMapping("/{id}")
fun getTodo(@PathVariable id: Long): Todo {
    return todoList.filter { it.id == id }.firstOrNull()
}

他には、JavaのOptionalクラスを利用している場合も注意が必要です。Optionalクラスを利用しているJavaコードをそのまま変換すると以下のように変換されます。

変換前のJavaコード:

public Optional<Hoge> findOne(long id) {
    return Optional.ofNullable(new HogeDTO(hogeRepo.findOne(id)));  
}

変換後のKotlinコード:

fun findOne(id: Long): Optional<HogeDTO> {
    return Optional.ofNullable(HogeDTO(hoge))
}

KotlinはJavaのOptionalクラスを利用しなくてもNull安全に書けるので上記のコードは以下のように変換できます。

fun findOne(id: Long): HogeDTO? {  
     return hogeRepo.findByIdOrNull(id)?.let { HogeDTO(it) }
}

JavaのOptionalからKotlinのNull安全機能の変換については以下の記事がとても参考になります。

https://typealias.com/guides/java-optionals-and-kotlin-nulls/

大まかな変換についてはInteliJの機能におまかせして、細かな部分を人間の手で修正する形で変換を行っていきました。他にもこの時点で不要なコードも極力削除しました。

このように移行作業を行っていった結果、既存のJavaコードをすべてKotlinに変換する事ができました。また、全体のコード行数も30%程度削減できました。

移行前:
classmethod-aws_portnoy-v2__サービス開発室_CMAPI_V2

移行後:
classmethod-aws_portnoy__サービス開発室_CMP_API

今後は、Javaの旧APIとKotlinの新APIを並行稼働しつつ徐々に新APIに移行していく予定です。

まとめ

クラスメソッドメンバーズのバックエンドAPIのリプレイスについて簡単ですが紹介させていただきました。KotlinはJetBrainsが開発していることもあり、Kotlinに関するInteliJの機能が充実していて、InteliJのおかげでかなり楽をして移行ができたんじゃないかなと思います。また新規プロジェクトベースでリプレイスを進めたので、既存のAPIの影響を考えなくてよくドラスティックに修正を行えました。他にもSpring Boot 3系をベースに実装したので2系から3系へのマイグレーションを特に意識せずに行えたことも大きかったとおもいます。

今回のリプレイスでテスト環境の整備もすることができましたが、それについてはまた別の記事で紹介したいなと思っています。

この記事をシェアする

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.